/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2017 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.pentaho.di.core.plugins;

import cn.hutool.core.util.URLUtil;
import com.google.common.collect.Maps;
import org.yzbdl.lanius.core.constant.LocalDir;
import org.yzbdl.lanius.core.plugins.ClassLoaderCfg;
import org.yzbdl.lanius.core.plugins.metastore.LdrPluginEntity;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.net.URLDecoder;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This describes the plugin itself, the IDs it listens too, what libraries (jar
 * files) it uses, the names, the i18n details, etc.
 *
 * @author matt
 *
 */
public class Plugin implements PluginInterface, PluginUniqueCode {

	private String id;
	private String name;
	private String version;
	private boolean enterprise;
	private String imageFile;
	private String category;
	private String description;
	private boolean nativePlugin;
	private Class<? extends PluginTypeInterface> pluginType;
	private List<String> libraries;
	private Class<?> mainType;
	private URL pluginFolder;

	/**
	 * 根据代码跟踪发现，返回的Map用于记录插件中一些抽象接口与具体实现类之间的关联映射关系<br/>
	 * 例如：<br/>
	 * StepMetaInterface.class --->
	 * org.pentaho.di.trans.steps.tableinput.TableInputMeta <br/>
	 * StepMetaInterface.class --->
	 * org.pentaho.di.trans.steps.selectvalues.SelectValuesMeta <br/>
	 * RepositoryMeta.class --->
	 * org.pentaho.di.repository.kdr.KettleDatabaseRepositoryMeta <br/>
	 * RepositoryDialogInterface.class --->
	 * lib/org.pentaho.di.ui.repository.pur.PurRepositoryDialog
	 */
	private Map<Class<?>, String> classMap;

	private ClassLoaderCfg classLoaderCfg;
	private boolean separateClassLoaderNeeded;
	private String classLoaderGroup;

//  private String[] ids;
//	private String errorHelpFile;

	/**
	 * @param pluginType
	 * @param category
	 * @param name
	 * @param description
	 * @param imageFile
	 * @param separateClassLoaderNeeded
	 * @param nativePlugin
	 * @param classMap
	 * @param libraries
	 */
	public Plugin(String id, Class<? extends PluginTypeInterface> pluginType, Class<?> mainType, String category,
	      String name, String description, String imageFile, boolean separateClassLoaderNeeded, boolean nativePlugin,
	      Map<Class<?>, String> classMap, List<String> libraries, URL pluginFolder, String version, boolean enterprise,
	      ClassLoaderCfg classLoaderCfg) {
		this(id, pluginType, mainType, category, name, description, imageFile, separateClassLoaderNeeded, null,
		      nativePlugin, classMap, libraries, pluginFolder, version, enterprise, classLoaderCfg);
	}

	/**
	 * @param id
	 * @param pluginType
	 * @param mainType
	 * @param category
	 * @param name
	 * @param description
	 * @param imageFile
	 * @param separateClassLoaderNeeded
	 * @param classLoaderGroup
	 * @param nativePlugin
	 * @param classMap
	 * @param libraries
	 * @param pluginFolder
	 */
	public Plugin(String id, Class<? extends PluginTypeInterface> pluginType, Class<?> mainType, String category,
	      String name, String description, String imageFile, boolean separateClassLoaderNeeded, String classLoaderGroup,
	      boolean nativePlugin, Map<Class<?>, String> classMap, List<String> libraries, URL pluginFolder, String version,
		  boolean enterprise, ClassLoaderCfg classLoaderCfg) {
		this.id = id;
		this.pluginType = pluginType;
		this.mainType = mainType;
		this.category = category;
		this.name = name;
		this.description = description;
		this.imageFile = imageFile;
		this.separateClassLoaderNeeded = separateClassLoaderNeeded;
		this.classLoaderGroup = classLoaderGroup;
		this.nativePlugin = nativePlugin;
		this.classMap = classMap;
		this.libraries = libraries;
//		this.errorHelpFile = errorHelpFile;
		this.pluginFolder = pluginFolder;
		this.version = version;
		this.enterprise = enterprise;
		this.classLoaderCfg = classLoaderCfg;
	}

	/**
	 * 从PluginEntity生成Plugin
	 *
	 * @param entity
	 * @return
	 */
	public static Plugin from(LdrPluginEntity entity) {
		Plugin plugin = fromWithOutClassMap(entity);

		// 填充classMap属性， 该属性的填充需要加载一些插件类， 需要注意的是这些插件类往往只能通过关联的类加载器进行加载。
		Map<Class<?>, String> classMap = Maps.newHashMap();
		Class<?> mainType = entity.getMainClassTypeClass();
		Class<? extends PluginTypeInterface> pluginType = entity.getPluginTypeClass();
		classMap.put(mainType, entity.getMainClassName());
		Class<?> clazz = entity.getMainClass();
		Annotation annotation = null;
		try {
			annotation = clazz.getAnnotation(entity.getPluginAnnotationTypeClass());
		} catch (Exception e) {
			return null;
		}
		entity.getPluginTypeInstance().addExtraClasses(classMap, clazz, annotation);
		plugin.setClassMap(classMap);

		return plugin;
	}

	/**
	 * <pre>
	 * 从PluginEntity生成Plugin, 但是不填充plugin的classMap
	 * 用于查询或初始化类加载器
	 * </pre>
	 *
	 * @return
	 */
	public static Plugin fromWithOutClassMap(LdrPluginEntity entity) {
		String id = entity.getPluginId();
		Class<? extends PluginTypeInterface> pluginType = entity.getPluginTypeClass();
		Class<?> mainType = entity.getMainClassTypeClass();
		String category = entity.getCategory();
		String name = entity.getName();
		String description = entity.getProfile();
		String imageFile = entity.getImage();
		boolean separateClassLoaderNeeded = entity.getClSeparateClassLoaderNeeded();
		boolean nativePlugin = entity.getIsNativePlugin();
		List<String> libraries = Stream.of(entity.getLibraryArray())
		      .map(libName -> LocalDir.LIB_PLUGINS + LocalDir.separator + libName).collect(Collectors.toList());
		URL pluginFolder = URLUtil.getURL(new File(LocalDir.PLUGIN + LocalDir.separator + entity.getFolderName()));
		String version = entity.getVersion();
		boolean enterprise = null!=entity.getEnterprise()?entity.getEnterprise():false;
		ClassLoaderCfg clCfg = new ClassLoaderCfg();
		clCfg.setIsSeparateClassLoaderNeeded(entity.getClSeparateClassLoaderNeeded());
		clCfg.setUserDefinedGroup(entity.getClUserDefinedGroup());
		clCfg.setFolderBasedGroup(entity.getClFolderGroup());
		Plugin plugin = new Plugin(id, pluginType, mainType, category, name, description, imageFile,
		      separateClassLoaderNeeded, nativePlugin, null, libraries, pluginFolder, version, enterprise, clCfg);
		return plugin;
	}

	@Override
	public String toString() {
		return id + "/" + version + "/" + name + "{" + pluginType + "}";
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Plugin)) {
			return false;
		}

		Plugin other = (Plugin) obj;

		// All the IDs have to be the same to match, otherwise it's a different plugin
		// This might be a bit over the top, usually we only have a single ID
		//
		if (pluginType == null && other.pluginType != null) {
			return false;
		}
		if (!pluginType.equals(other.pluginType)) {
			return false;
		}
		if (id == null && other.id != null) {
			return false;
		}
		if (!id.equals(other.id)) {
			return false;
		}
		if (version == null && other.version != null) {
			return false;
		}
		if (!version.equals(other.version)) {
			return false;
		}
		if (enterprise!=other.enterprise) {
			return false;
		}

		return true;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((pluginType == null) ? 0 : pluginType.hashCode());
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		result = prime * result + ((version == null) ? 0 : version.hashCode());
		return result;
	}

	/**
	 * @return the category
	 */
	@Override
	public String getCategory() {
		return category;
	}

	/**
	 * @param category the category to set
	 */
	public void setCategory(String category) {
		this.category = category;
	}

	/**
	 * @return the name
	 */
	@Override
	public String getName() {
		return name;
	}

	/**
	 * @param name the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * @return the description
	 */
	@Override
	public String getDescription() {
		return description;
	}

	/**
	 * @param description the description to set
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * @return the id
	 */
	@Override
	public String getId() {
		return id;
	}

	/**
	 * @return the pluginType
	 */
	@Override
	public Class<? extends PluginTypeInterface> getPluginType() {
		return pluginType;
	}

	/**
	 * @param pluginType the pluginType to set
	 */
	public void setPluginType(Class<? extends PluginTypeInterface> pluginType) {
		this.pluginType = pluginType;
	}

	/**
	 * @return the imageFile
	 */
	@Override
	public String getImageFile() {
		return imageFile;
	}

	/**
	 * @param imageFile the imageFile to set
	 */
	public void setImageFile(String imageFile) {
		this.imageFile = imageFile;
	}

	/**
	 * @return the separateClassLoaderNeeded
	 */
	@Override
	public boolean isSeparateClassLoaderNeeded() {
		return separateClassLoaderNeeded;
	}

	/**
	 * @param separateClassLoaderNeeded the separateClassLoaderNeeded to set
	 */
	public void setSaperateClassLoaderNeeded(boolean separateClassLoaderNeeded) {
		this.separateClassLoaderNeeded = separateClassLoaderNeeded;
	}

	/**
	 * @return the nativePlugin
	 */
	@Override
	public boolean isNativePlugin() {
		return nativePlugin;
	}

	/**
	 * @param nativePlugin the nativePlugin to set
	 */
	public void setNativePlugin(boolean nativePlugin) {
		this.nativePlugin = nativePlugin;
	}

	/**
	 * @return the classMap
	 */
	@Override
	public Map<Class<?>, String> getClassMap() {
		return classMap;
	}

	/**
	 * @param classMap the classMap to set
	 */
	public void setClassMap(Map<Class<?>, String> classMap) {
		this.classMap = classMap;
	}

	/**
	 * @return the libraries
	 */
	@Override
	public List<String> getLibraries() {
		return libraries;
	}

	/**
	 * @param libraries the libraries to set
	 */
	public void setLibraries(List<String> libraries) {
		this.libraries = libraries;
	}

	@Override
	public Class<?> getMainType() {
		return mainType;
	}

	@Override
	public URL getPluginDirectory() {
		return this.pluginFolder;
	}

	@Override
	public String getPluginDirectoryString() {
		try {
			return URLDecoder.decode(this.pluginFolder.getPath(),"UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			return null;
		}
	}

	@Override
	public String getClassLoaderGroup() {
		return classLoaderGroup;
	}

	@Override
	public void setClassLoaderGroup(String classLoaderGroup) {
		this.classLoaderGroup = classLoaderGroup;
	}

	@Override
	public String getVersion() {
		return version;
	}

	@Override
	public void setVersion(String version) {
		this.version = version;
	}

	@Override
	public ClassLoaderCfg getClassLoaderCfg() {
		return classLoaderCfg;
	}

	@Override
	public String getPluginId() {
		return this.getId();
	}

	@Override
	public boolean isEnterprise() {
		return enterprise;
	}

	@Override
	public void setEnterprise(boolean enterprise) {
		this.enterprise = enterprise;
	}
}
